package cloudspace.ui.zhtml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
import javassist.expr.FieldAccess;
import javassist.expr.MethodCall;
import javassist.expr.NewExpr;

import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuService;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.WebApp;
import org.zkoss.zk.ui.util.Clients;

import cloudspace.ui.applet.AppletApplicationSupportStrategy;
import cloudspace.ui.applet.AppletDistributedMap;
import cloudspace.ui.applet.AppletProxyFactory;
import cloudspace.ui.applet.AppletVM;
import cloudspace.ui.applet.CloudResult;
import cloudspace.ui.applet.CloudToApplet;
import cloudspace.ui.applet.io.CloudFile;
import cloudspace.ui.applet.io.CloudFileInputStream;
import cloudspace.ui.applet.io.CloudFileOutputStream;
import cloudspace.ui.applet.io.CloudFileReader;
import cloudspace.ui.applet.io.CloudFileWriter;
import cloudspace.ui.applet.io.CommandHandler;
import cloudspace.ui.applet.lang.CloudSystem;
import cloudspace.util.ClassFinder;
import cloudspace.util.ClassFinder.DirectoryResources;
import cloudspace.vm.VM;


public class Applet extends org.zkoss.zul.Applet
{
    /**
     * 
     */
    private static final long serialVersionUID = -5927307297877213609L;

    private static final boolean DEBUG_MODE = false;

    private Logger log = Logger.getLogger( Applet.class );

    private boolean hasJava = false;


    private class AppletEventListener implements AuService
    {
        Map<String, CommandHandler> eventToHandler = new HashMap<String, CommandHandler>();


        public AppletEventListener()
        {
            safePutAll( CloudFileInputStream.class.getName(),
                CloudFileInputStream.getCommandHandlers() );
            safePutAll( CloudFileOutputStream.class.getName(),
                CloudFileOutputStream.getCommandHandlers() );
            safePutAll( CloudFile.class.getName(),
                CloudFile.getCommandHandlers() );
            safePutAll( CloudSystem.class.getName(),
                CloudSystem.getCommandHandlers() );
            safePutAll( AppletApplicationSupportStrategy.class.getName(),
                AppletApplicationSupportStrategy.getCommandHandlers() );
            safePutAll( AppletDistributedMap.class.getName(),
                AppletDistributedMap.getCommandHandlers() );

        }


        private void safePutAll( String id, Map<String, CommandHandler> handlers )
        {
            for ( String key : handlers.keySet() )
            {
                if ( eventToHandler.containsKey( key ) )
                    throw new RuntimeException( "Namespace Conflict " + key
                        + " is already bound. Not binding " + id );
            }
            eventToHandler.putAll( handlers );
        }


        public boolean service( AuRequest request, boolean everError )
        {
            try
            {
                VM.lookupVM().enter();
                @SuppressWarnings("rawtypes")
                Map data = request.getData();
                String reqId = (String)data.get( "reqId" );
                CommandHandler handler = eventToHandler.get( request.getCommand() );
                if ( handler != null )
                {
                    hasJava = true;
                    CloudToApplet result;
                    try
                    {
                        Map attr = Executions.getCurrent()
                        /* .getDesktop() */.getSession().getAttributes();
                        result = handler.handleRequest( data, attr );
                    }
                    catch ( Throwable t )
                    {
                        result = new CloudToApplet( t );
                    }
                    String requestScript = result.getRequestScript( reqId );
                    Clients.evalJavaScript( requestScript );
                }
                else if ( request.getCommand().equals( "onSystemOut" ) )
                {
                    String message = (String)data.get( "message" );
                    System.out.print( message );
                    CloudToApplet comm = new CloudToApplet();
                    String requestScript = comm.getRequestScript( reqId );
                    Clients.evalJavaScript( requestScript );
                }
                else if ( request.getCommand().equals( "onSystemErr" ) )
                {
                    String message = (String)data.get( "message" );
                    System.err.print( message );
                }
                else
                {
                    return false;
                }
                return true;
            }
            finally
            {
                VM.leave();
            }
        }

    }


    public Applet()
    {
        super();
        this.setParam( "uuid", this.getUuid() );
        Executions.getCurrent()
            .getDesktop()
            .addListener( new AppletEventListener() );
    }


    // http://www.rgagnon.com/javadetails/java-0483.html
    static public boolean deleteDirectory( File path )
    {
        if ( path.exists() )
        {
            File[] files = path.listFiles();
            for ( int i = 0; i < files.length; i++ )
            {
                if ( files[i].isDirectory() )
                {
                    deleteDirectory( files[i] );
                }
                else
                {
                    files[i].delete();
                }
            }
        }
        return ( path.delete() );
    }


    protected void renderProperties(
        org.zkoss.zk.ui.sys.ContentRenderer renderer )
        throws java.io.IOException
    {
        String codebase = super.getCodebase();
        if ( codebase == null )
        {
            super.setCodebase( "APPLET" );
            codebase = "APPLET";
        }
        else
        {
            super.setCodebase( codebase + "/APPLET" );
            codebase += "/APPLET";
        }
        super.renderProperties( renderer );
        String localPath = new File( super.getPage().getRequestPath() ).getParent();
        String localRoot = Executions.getCurrent()
            .getDesktop()
            .getWebApp()
            .getRealPath( localPath )
            + File.separator + codebase;
        try
        {
            VM executingVM = VM.lookupVM();
            if ( executingVM != null )
            {
                // String localRoot = executingVM.getLocalRoot();
                // File root = new File( localRoot );
                File appletDir = new File( localRoot );
                DirectoryResources resources = ClassFinder.buildSource( new File( localRoot ).getParentFile(),
                    "APPLET" );
                if ( resources.haveCompiledFilesChanged( appletDir.lastModified() )
                    || DEBUG_MODE )
                {
                    deleteDirectory( appletDir );
                    appletDir.mkdir();
                    List<File> classes = resources.getClasses();
                    List<File> libraries = resources.getLibraries();
                    ClassPool pool = new ClassPool();
                    pool.appendSystemPath();
                    WebApp webapp = Executions.getCurrent()
                        .getDesktop()
                        .getWebApp();
                    pool.appendClassPath( webapp.getRealPath( "/WEB-INF/classes/" ) );

                    addProxies( pool, appletDir.getAbsolutePath() );
                    HashSet<String> classSet = new HashSet<String>();
                    List<CtClass> studentCtClass = new ArrayList<CtClass>();
                    for ( File libraryFile : libraries )
                    {
                        try
                        {
                            JarFile jar = new JarFile( libraryFile );
                            Enumeration<JarEntry> enumer = jar.entries();
                            while ( enumer.hasMoreElements() )
                            {
                                JarEntry entry = enumer.nextElement();
                                if ( !entry.isDirectory()
                                    && entry.getName().endsWith( ".class" ) )
                                {
                                    InputStream classInput = jar.getInputStream( entry );
                                    CtClass clazz = pool.makeClass( classInput );
                                    studentCtClass.add( clazz );
                                    classInput.close();
                                }
                            }
                        }
                        catch ( IOException e )
                        {
                            log.error( "Error reading a jar file for an applet",
                                e );
                        }
                    }
                    for ( File classFile : classes )
                    {
                        FileInputStream input = new FileInputStream( classFile );
                        CtClass clazz = pool.makeClass( input );
                        studentCtClass.add( clazz );
                        classSet.add( clazz.getName() );
                        input.close();
                    }
                    CtClass superClazz = pool.get( java.applet.Applet.class.getName() );
                    for ( CtClass clazz : studentCtClass )
                    {
                        if ( clazz.subclassOf( superClazz ) )
                        {
                            CtMethod initMethod = null;

                            try
                            {
                                initMethod = clazz.getMethod( "init", "()V" );
                            }
                            catch ( NotFoundException e )
                            {
                                // Do Nothing leave it as null
                            }
                            // This shouldnt ever be null because java.applet.
                            // Applet is the super class and it has a dummy
                            // implementation.
                            if ( initMethod == null
                                || !initMethod.getDeclaringClass()
                                    .equals( clazz ) )
                            {
                                initMethod = CtMethod.make( "public void init(){super.init();}",
                                    clazz );
                                clazz.addMethod( initMethod );
                            }
                            initMethod.insertBefore( AppletVM.class.getName()
                                + ".init(this);" );
                            CtMethod resultCatcher = CtMethod.make( "public void notifyResult( String reqId, String[] response ){"
                                + AppletVM.class.getName()
                                + ".gotResponse( reqId, new "
                                + CloudResult.class.getName()
                                + "( response ) );}",
                                clazz );
                            clazz.addMethod( resultCatcher );
                            CtMethod errorCatcher = CtMethod.make( "public void notifyError( String reqId, String[] response){"
                                + "Class exType;"
                                + "try{exType = Class.forName( response[0] );}"
                                + "catch ( Exception e ){exType = Throwable.class;}"
                                + AppletVM.class.getName()
                                + ".gotResponse( reqId, new "
                                + CloudResult.class.getName()
                                + "( exType, response[1] ) );}",
                                clazz );
                            clazz.addMethod( errorCatcher );
                        }
                        addWriteMethod( clazz, classSet );
                        addReadMethod( clazz, classSet );
                        clazz.instrument( new AppletExpressionEditor( pool ) );
                        clazz.writeFile( appletDir.getAbsolutePath() );
                    }

                    String actualLocation = Executions.getCurrent()
                        .getDesktop()
                        .getWebApp()
                        .getRealPath( "/WEB-INF/classes/" );
                    FileUtils.copyDirectory( new File( actualLocation,
                        "cloudspace/ui/applet" ), new File( appletDir,
                        "cloudspace/ui/applet" ) );
                    FileUtils.copyDirectory( new File( actualLocation,
                        "cloudspace/util" ), new File( appletDir,
                        "cloudspace/util" ) );
                    File from = new File( Executions.getCurrent()
                        .getDesktop()
                        .getWebApp()
                        .getRealPath( "/WEB-INF/appletlib" ) );
                    File to = new File( appletDir.getAbsolutePath() );
                    FileUtils.copyDirectory( from, to );
                }
            }
        }
        catch ( Exception e )
        {
            e.printStackTrace();
        }
    }


    private void addReadMethod( CtClass clazz, HashSet<String> classSet )
    {
        try
        {
            if ( clazz.getDeclaredMethod( "_get_value_" ) != null )
                return;
        }
        catch ( NotFoundException e )
        {
            // Do nothing
        }
        try
        {
            CtField[] fields = clazz.getDeclaredFields();
            String methodBody = "public Object _get_value_(String fieldName){";
            for ( CtField field : fields )
            {
                methodBody += "if(fieldName.equals(\"" + field.getName()
                    + "\"))";
                methodBody += "return " + wrapField( field ) + ";";
            }
            CtClass superClazz = clazz.getSuperclass();
            if ( superClazz != null )
            {
                try
                {
                    CtMethod superMeth = superClazz.getMethod( "_get_value_",
                        "(Ljava/lang/String;)Ljava/lang/Object;" );
                    methodBody += "return super._get_value_(fieldName);}";
                }
                catch ( NotFoundException e )
                {
                    if ( classSet.contains( superClazz.getName() ) )
                    {
                        addReadMethod( superClazz, classSet );
                    methodBody += "return super._get_value_(fieldName);}";
                    }
                    else
                    {
                        methodBody += "return null;}";
                    }
                }
            }
            else
            {
                methodBody += "return null;}";
            }
            CtMethod meth = CtMethod.make( methodBody, clazz );
            clazz.addMethod( meth );
        }
        catch ( CannotCompileException e )
        {
//            log.error( "Could not compile read method",e );
        }
        catch ( NotFoundException e )
        {
            // Our class path might not have this class. Lets just avoid it and
            // have xstream refuse to reflect on it.
            // e.printStackTrace();
        }

    }


    private String wrapField( CtField field ) throws NotFoundException
    {
        CtClass returnValue = field.getType();
        if ( returnValue.equals( CtClass.booleanType ) )
            return "new Boolean(" + field.getName() + ")";
        else if ( returnValue.equals( CtClass.byteType ) )
            return "new Byte(" + field.getName() + ")";
        else if ( returnValue.equals( CtClass.intType ) )
            return "new Integer(" + field.getName() + ")";
        else if ( returnValue.equals( CtClass.charType ) )
            return "new Char(" + field.getName() + ")";
        else if ( returnValue.equals( CtClass.doubleType ) )
            return "new Double(" + field.getName() + ")";
        else if ( returnValue.equals( CtClass.floatType ) )
            return "new Float(" + field.getName() + ")";
        else if ( returnValue.equals( CtClass.longType ) )
            return "new Long(" + field.getName() + ")";
        else if ( returnValue.equals( CtClass.shortType ) )
            return "new Short(" + field.getName() + ")";
        return field.getName();
    }


    private String unwrapField( CtField field, String value )
        throws NotFoundException
    {
        CtClass returnValue = field.getType();
        if ( returnValue.equals( CtClass.booleanType ) )
            return "((Boolean)" + value + ").booleanValue()";
        else if ( returnValue.equals( CtClass.byteType ) )
            return "((Byte)" + value + ").byteValue()";
        else if ( returnValue.equals( CtClass.intType ) )
            return "((Integer)" + value + ").intValue()";
        else if ( returnValue.equals( CtClass.charType ) )
            return "((Char)" + value + ").charValue()";
        else if ( returnValue.equals( CtClass.doubleType ) )
            return "((Double)" + value + ").doubleValue()";
        else if ( returnValue.equals( CtClass.floatType ) )
            return "((Float)" + value + ").floatValue()";
        else if ( returnValue.equals( CtClass.longType ) )
            return "((Long)" + value + ").longValue()";
        else if ( returnValue.equals( CtClass.shortType ) )
            return "((Short)" + value + ").shortValue()";
        return value;
    }


    private void addWriteMethod( CtClass clazz, HashSet<String> classSet )
    {
        try
        {
            if ( clazz.getDeclaredMethod( "_write_field_" ) != null )
                return;
        }
        catch ( NotFoundException e )
        {
            // Do nothing
        }
        try
        {
            CtField[] fields = clazz.getDeclaredFields();
            String methodBody = "public void _write_field_(String fieldName, Object value){";
            for ( CtField field : fields )
            {
                methodBody += "if(fieldName.equals(\"" + field.getName()
                    + "\"))\n";
                methodBody += "{\n";
//                methodBody += "System.out.println(\"Writing " + field.getName()
//                    + "\");";
                methodBody += field.getName() + "=("
                    + field.getType().getName() + ")"
                    + unwrapField( field, "value" ) + ";\n";
                methodBody += "return;\n";
                methodBody += "}\n";
            }
            CtClass superClazz = clazz.getSuperclass();
            if ( superClazz != null )
            {
                try
                {
                    // CtMethod[] methods = clazz.getMethods();
                    superClazz.getMethod( "_write_field_",
                        "(Ljava/lang/String;Ljava/lang/Object;)V" );
                    methodBody += "super._write_field_(fieldName,value);";
                }
                catch ( NotFoundException e )
                {
                    if ( classSet.contains( superClazz.getName() ) )
                    {
                        addWriteMethod( superClazz, classSet );
                        methodBody += "super._write_field_(fieldName,value);";
                    }
                }
            }
            methodBody += "}";
            CtMethod meth = CtMethod.make( methodBody, clazz );
            clazz.addMethod( meth );
        }
        catch ( CannotCompileException e )
        {
//            log.error( "Could not compile write method" );
        }
        catch ( NotFoundException e )
        {
            // Our class path might not have this class. Lets just avoid it and
            // have xstream refuse to reflect on it.
            // log.error( "Could not find the type of a field" );
        }
    }


    public boolean isInstalled()
    {
        return hasJava;
    }


    private void addProxies( ClassPool pool, String dirToWrite )
    {
        try
        {
            CtClass proxy = AppletProxyFactory.generateProxy( pool.get( "java.io.FileWriter" ),
                pool.get( CloudFileWriter.class.getName() ),
                pool );
            proxy.writeFile( dirToWrite );
            proxy = AppletProxyFactory.generateProxy( pool.get( "java.io.FileReader" ),
                pool.get( CloudFileReader.class.getName() ),
                pool );
            proxy.writeFile( dirToWrite );
            proxy = AppletProxyFactory.generateProxy( pool.get( "java.io.FileInputStream" ),
                pool.get( CloudFileInputStream.class.getName() ),
                pool );
            proxy.writeFile( dirToWrite );
            proxy = AppletProxyFactory.generateProxy( pool.get( "java.io.FileOutputStream" ),
                pool.get( CloudFileOutputStream.class.getName() ),
                pool );
            proxy.writeFile( dirToWrite );
        }
        catch ( NotFoundException e )
        {
//            log.error( "Could not find FileReader class in class pool.", e );
        }
        catch ( CannotCompileException e )
        {
//            log.error( "Could not compile one of the proxied classes.", e );
        }
        catch ( IOException e )
        {
//            log.error( "Could not write the proxy to disk" );
        }
    }


    private class AppletExpressionEditor extends ExprEditor
    {
        private ClassPool pool;

        private Map<CtClass, Class<?>> replacement = new HashMap<CtClass, Class<?>>();

        private Set<String> toProxy = new HashSet<String>();


        public AppletExpressionEditor( ClassPool pool )
        {
            this.pool = pool;
            try
            {
                toProxy.add( FileInputStream.class.getName() );
                toProxy.add( FileOutputStream.class.getName() );
                toProxy.add( FileReader.class.getName() );
                toProxy.add( FileWriter.class.getName() );

                replacement.put( pool.get( FileInputStream.class.getName() ),
                    CloudFileInputStream.class );
                replacement.put( pool.get( FileOutputStream.class.getName() ),
                    CloudFileOutputStream.class );
                replacement.put( pool.get( File.class.getName() ),
                    CloudFile.class );
                replacement.put( pool.get( FileReader.class.getName() ),
                    CloudFileReader.class );
                replacement.put( pool.get( FileWriter.class.getName() ),
                    CloudFileWriter.class );
            }
            catch ( NotFoundException e )
            {
//                log.error( "Could not get java.io classes out of the class pool",
//                    e );
            }
        }


        public void edit( MethodCall call )
        {
            try
            {
                // String methodName = call.getMethodName();

                String declaringClassName = call.getClassName();
                if ( toProxy.contains( declaringClassName ) )
                {
                    String toReplace = "";
                    if ( !call.getMethod()
                        .getReturnType()
                        .equals( CtClass.voidType ) )
                    {
                        toReplace = "$_ = ";
                    }
                    String simpleName = declaringClassName.substring( declaringClassName.lastIndexOf( "." ) + 1,
                        declaringClassName.length() );
                    toReplace += "_Proxy_" + simpleName + "."
                        + call.getMethodName() + "($0,$$);";
                    call.replace( toReplace );
                }
                if ( declaringClassName.equals( "java.lang.System" ) )
                {
                    CtMethod method = call.getMethod();
                    if ( method.getName().equals( "getProperty" ) )
                    {
                        call.replace( "$_ = cloudspace.ui.applet.lang.CloudSystem.getProperty($$);" );
                    }
                    if ( method.getName().equals( "setProperty" ) )
                    {
                        call.replace( "$_ = cloudspace.ui.applet.lang.CloudSystem.setProperty($$);" );
                    }
                    if ( method.getName().equals( "getProperties" ) )
                    {
                        call.replace( "$_ = new java.util.Properties();" );
                    }
                }
            }
            catch ( CannotCompileException e )
            {
//                log.error( "Could not compile the Proxy for the method call", e );
            }
            catch ( NotFoundException e )
            {
//                log.error( "Could not get the method for the method call we are proxying "
//                    + call.getClassName() );
            }
        }


        public void edit( NewExpr call )
        {
            try
            {
                CtClass clazz = call.getConstructor().getDeclaringClass();
                if ( replacement.containsKey( clazz ) )
                {
                    Class<?> toReplaceWith = replacement.get( clazz );
                    call.replace( "$_ = new " + toReplaceWith.getName()
                        + "($$);" );
                }
            }
            catch ( CannotCompileException e )
            {
//                log.error( "Could not compile the new NewExpr ", e );
            }
            catch ( NotFoundException e )
            {
//                log.error( "Could not find the constructor for the new expression of "
//                    + call.getClassName() );
            }
        }


        public void edit( FieldAccess call )
        {

            try
            {
                CtClass systemClazz = pool.get( "java.lang.System" );
                CtClass fileClazz = pool.get( "java.io.File" );
                CtClass clazz = call.getField().getDeclaringClass();
                if ( clazz.equals( systemClazz )
                    && call.getField().getName().equals( "out" ) )
                    call.replace( "$_ = " + AppletVM.class.getName() + ".out;" );
                else if ( clazz.equals( systemClazz )
                    && call.getField().getName().equals( "err" ) )
                    call.replace( "$_ = " + AppletVM.class.getName() + ".err;" );
                else if ( clazz.equals( fileClazz )
                    && call.getField().getName().equals( "separator" ) )
                    call.replace( "$_ = " + CloudFile.class.getName()
                        + ".separator;" );
            }
            catch ( NotFoundException e )
            {
               log.error( "Class not available in class path "
                    + call.getClassName() );
            }
            catch ( CannotCompileException e )
            {
                log.error( "Could not compile the System.err and System.out injection code",
                    e );
            }

        }
    }
}
